/*
 * Copyright (c) 2008 Golden T Studios.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.golden.gamedev;

// JFC
import java.awt.*;
import java.awt.event.*;
import java.util.StringTokenizer;
import javax.swing.JOptionPane;

// GTGE
import com.golden.gamedev.engine.BaseGraphics;
import com.golden.gamedev.engine.graphics.AppletMode;
import com.golden.gamedev.engine.graphics.FullScreenMode;
import com.golden.gamedev.engine.graphics.WindowedMode;
import com.golden.gamedev.engine.graphics.WindowExitListener;
import com.golden.gamedev.funbox.ErrorNotificationDialog;


/**
 * <code>GameLoader</code> class is the class that manages <code>Game</code>
 * class initialization. <p>
 *
 * <code>GameLoader</code> handles <code>Game</code> class
 * {@linkplain com.golden.gamedev.engine.graphics graphics engine}
 * initialization, validating {@linkplain #MINIMUM_VERSION user Java version},
 * and also catch any unexpected runtime error in <code>Applet</code> mode. <p>
 *
 * Example of how-to-use <code>GameLoader</code> in application mode :
 * <pre>
 *    public class YourGame extends Game {
 *
 *       public static void main(String[] args) {
 *          <b>GameLoader</b> game = new GameLoader();
 *
 *          // init game with fullscreen mode, 640x480 screen resolution
 *          game.setup(<b>new YourGame()</b>, new Dimension(640,480), true);
 *
 *          game.start();
 *       }
 *
 *    }
 * </pre>
 * <p>
 *
 * In applet mode, <code>GameLoader</code> class need to be subclassed and
 * override {@link #createAppletGame()} method to return the actual game :
 * <pre>
 *    public class YourGameApplet extends GameLoader {
 *
 *       protected Game createAppletGame() {
 *          return new YourGame();
 *       }
 *
 *    }
 * </pre>
 * Then point the applet tag code to that <code>GameLoader</code> subclass : <br>
 * (html page)
 * <pre>
 *    &lt;html&gt;
 *    &lt;head&gt;
 *    &lt;/head&gt;
 *    &lt;body&gt;
 *
 *    &lt;applet code="<b>gamepackage.YourGameApplet.class</b>"
 *            archive="<b>yourgamearchive.jar,golden_x_x_x.jar</b>"
 *            width=640 height=480&gt;
 *    &lt;/applet&gt;
 *
 *    &lt;/body&gt;
 *    &lt;/html&gt;
 * </pre>
 *
 * @see #setup(Game, Dimension, boolean, boolean)
 * @see #start()
 * @see #createAppletGame()
 */
public class GameLoader extends AppletMode implements WindowListener, Runnable {


 /*************************** VERSION VALIDATOR ******************************/

	/**
	 * The installed Java Virtual Machine (JVM) version on player machine.
	 */
	public static final String JAVA_VERSION;

	static {
		String ver = "1.4.1";

		try { ver = System.getProperty("java.version");
		} catch (Exception e) { }

		JAVA_VERSION = ver;
	}

	/**
	 * The minimum Java version that the game known can be run properly.
	 * The default is 1.4. <p>
	 *
	 * How to change the minimum Java version :
	 * <pre>
	 *    GameLoader game = new GameLoader();
	 *    game.MINIMUM_VERSION = "1.4.2";
	 *    game.setup(....);
	 *    game.start();
	 * </pre>
	 *
	 * In applet mode, minimum version is specified in applet param 'MINIMUM'
	 * tag. <br>
	 * For example:
	 * <pre>
	 *    &lt;PARAM NAME="MINIMUM" VALUE="1.4.2"&gt;
	 * </pre>
	 */
	public String 		MINIMUM_VERSION = "1.4";

	private boolean		VALID_JAVA_VERSION = true;


 /***************************** INFO MESSAGES ********************************/

	private String[]	INFO_MSG = new String[]
								   { "Loading Game, please wait a moment" };


 /*************************** LOADER PROPERTIES ******************************/

	/**
	 * Graphics engine loaded by this <code>GameLoader</code>.
	 */
	protected BaseGraphics	gfx;

	/**
	 * The game (subclass of <code>Game</code> class) initialized by this
	 * <code>GameLoader</code>.
	 */
	protected Game			game;


 /****************************************************************************/
 /******************************* CONSTRUCTOR ********************************/
 /****************************************************************************/

	/**
	 * Constructs new <code>GameLoader</code>.
	 *
	 * @see #setup(Game, Dimension, boolean, boolean)
	 * @see #start()
	 */
	public GameLoader() {
	}


 /****************************************************************************/
 /************************** START / STOP GAME *******************************/
 /****************************************************************************/

	/**
	 * Starts the game that have been loaded by this loader.
	 *
	 * @see #setup(Game, Dimension, boolean, boolean)
	 */
	public void start() {
		if (!VALID_JAVA_VERSION) {
			// java version is not valid!
			return;
		}

   		if (gfx != null) {
	   		// graphics has been initialized
			// time to start the game
	   		if (game != null) {
			   game.start();
			}

			return;
		}


		try {
			// getParameter() will fail if this is not in applet environment
			String param = getParameter("MINIMUM_VERSION");

			if (param != null) {
				// developer has defined a custom minimum Java version
				MINIMUM_VERSION = param;
			}

		} catch (Exception e) {
			JOptionPane.showMessageDialog(null,
				"ERROR: GameLoader.setup(Game, Dimension, Fullscreen); need to be called\n" +
				"before calling GameLoader.start();",
				"Game Initialization", JOptionPane.ERROR_MESSAGE
			);

			System.exit(0);
		}


		//********** if above method successfully passed ***********//
		//********** this must be in applet environment! ***********//

		// java version validation
		if (!validJavaVersion()) {
			// not valid java version!!
			INFO_MSG = new String[]
				{ "Sorry, this game requires Java " + MINIMUM_VERSION + "++",
				  "Your machine only has Java " + JAVA_VERSION + " installed",
				  "",
				  "Install the latest Java Runtime Edition (JRE)",
				  "from http://www.java.com" };

			return;
		}


		try {
			if (game == null) {
				// time to initialize applet game
				game = createAppletGame();

				if (game == null) {
					// game failed to initialized
					JOptionPane.showMessageDialog(null,
						"FATAL ERROR: Game main-class is not specified!\n" +
						"Please subclass GameLoader class and override\n" +
						"createAppletGame() method to return your game main class.\n\n" +
						"For example :\n" +
						"public class YourGameApplet extends GameLoader {\n" +
						"   protected Game createAppletGame() {\n" +
						"      return new YourGame();\n" +
						"   }\n" +
						"}", "Game Initialization", JOptionPane.ERROR_MESSAGE);

					VALID_JAVA_VERSION = false;
					return;
				}

				gfx = this;
				game.bsGraphics = gfx;
			}

			// initialization of applet mode graphics engine
			super.start();

			// start the game
			new Thread(this).start();

		} catch (Throwable e) {
			INFO_MSG = new String[]
				{ "UNRECOVERABLE ERROR",
				  "PLEASE CONTACT THE GAME AUTHOR" };

			removeAll();

			// draw the error messages in standard paint() method
			setIgnoreRepaint(false);

			// show the exception dialog to the user
			new ErrorNotificationDialog(e, this, getClass().getName(), null);
		}
	}

	/**
	 * Stops the game from running, to resume the game call {@link #start()}
	 * again, please <b>see note</b> below. <p>
	 *
	 * By default this method is only called by browser in Applet Game. <p>
	 *
	 * Browser calls this method automatically when the web page contain the
	 * game is been replaced by another page and just before the applet is going
	 * to be destroyed. <br>
	 * The <code>start()</code> method is also automatically called by the
	 * browser each time the applet is being revisited.
	 */
	public void stop() {
		if (game != null) {
			game.stop();
		}
	}

	/**
	 * In applet environment the game have to be started in a thread, this is
	 * the thread implementation that actually launch the applet game.
	 */
	public final void run() {
		if (game != null) {
			game.start();
		}
	}

	/**
	 * To play game in applet environment, <code>GameLoader</code> class need to
	 * be subclassed and this method need to be override to return the actual
	 * game object. <p>
	 *
	 * For example :
	 * <pre>
	 * public class YourGameApplet extends GameLoader {
	 *
	 *    protected Game createAppletGame() {
	 *       return new YourGame();
	 *    }
	 *
	 * }
	 * </pre>
	 */
	protected Game createAppletGame() {
		try {
			String className = getParameter("GAME");
//			System.out.println(className);

			if (className != null) {
				if (className.endsWith(".class")) {
					// omit '.class'
					className = className.substring(0, className.length()-6);
				}

				Class mainClass = Class.forName(className);

				return (Game) mainClass.newInstance();
			}
		} catch (Throwable e) { }

		return null;
	}


 /****************************************************************************/
 /******************************* GAME SETUP *********************************/
 /****************************************************************************/

	/**
	 * Initializes graphics engine with specified size, mode, bufferstrategy,
	 * and associates it with specified <code>Game</code> object.
	 */
	public void setup(Game game, Dimension d,
					  boolean fullscreen,
					  boolean bufferstrategy) {
	try {
		// validate java version first
		if (!validJavaVersion()) {
			// not valid java version!!
			JOptionPane.showMessageDialog(null,
				"Sorry, this game requires Java "+MINIMUM_VERSION+"++ installed\n" +
				"Your machine only has Java "+JAVA_VERSION+" installed\n\n"+
				"Please install the latest Java Runtime Edition (JRE)\n" +
				"from http://www.java.com",
				"Game Initialization", JOptionPane.ERROR_MESSAGE
			);

			// don't bother to continue
			System.exit(-1);
		}

		// time to create the graphics engine
		if (fullscreen) {
			// fullscreen mode
			FullScreenMode mode = null;
			try {
				mode = new FullScreenMode(d, bufferstrategy);
				mode.getFrame().removeWindowListener(WindowExitListener.getInstance());
				mode.getFrame().addWindowListener(this);

				gfx = mode;
			} catch (Throwable e) {
				e.printStackTrace();

				JOptionPane.showMessageDialog(null,
											  "ERROR: Entering FullScreen Mode\n" +
											  "Caused by: " + e.toString(),
											  "Graphics Engine Initialization",
											  JOptionPane.ERROR_MESSAGE);
				// fail-safe
				fullscreen = false;

				if (mode != null) {
					mode.cleanup();
				}
			}
		}

		if (!fullscreen) {
			// windowed mode
			WindowedMode mode = new WindowedMode(d, bufferstrategy);
			mode.getFrame().removeWindowListener(WindowExitListener.getInstance());
			mode.getFrame().addWindowListener(this);

			gfx = mode;
		}

		this.game = game;
		this.game.bsGraphics = gfx;

	} catch (Throwable e) {
		e.printStackTrace();

		JOptionPane.showMessageDialog(null,
			"Fatal Error: Failed to initialize game environment!\n" +
			"Caused by:\n" +
			"       " + e + "\n" +
			"Please send above exception to the Game Author.\n",
			"Game Initialization", JOptionPane.ERROR_MESSAGE
		);

		System.exit(-1);
	} }

	/**
	 * Initializes graphics engine with specified size, mode, using
	 * bufferstrategy by default, and associates it with specified
	 * <code>Game</code> object.
	 */
	public void setup(Game game, Dimension d, boolean fullscreen) {
		setup(game, d, fullscreen, true);
	}

	/**
	 * Returns the game associated with this game loader or null if this game
	 * loader has not loaded any game.
	 */
	public Game getGame() {
		return game;
	}


 /****************************************************************************/
 /***************************** INFO NOTIFIER ********************************/
 /****************************************************************************/

	/**
	 * Draw essentials informations in applet game, for example :
	 * draw info when waiting the game to show up, or when the game is throwing
	 * an exception.
	 */
	public void paint(Graphics g) {
		// background
		int width = getSize().width,
			height = getSize().height;

		g.setColor(new Color(255,0,0));
		g.fillRect(0, 0, width, height);


		// draw the error messages
		g.setFont(new Font("Monospaced", Font.PLAIN, 16));
		FontMetrics fm = g.getFontMetrics();

		int y = (height/2) - ((fm.getHeight()+10)*(INFO_MSG.length/2));
		g.setColor(new Color(0,0,0));

		try {
			// for smoooth text :)
			((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
							   				  RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		} catch (Exception e) { }

		for (int i=0;i < INFO_MSG.length;i++) {
			g.drawString(INFO_MSG[i],
				(width/2) - (fm.stringWidth(INFO_MSG[i])/2), y);

			y += fm.getHeight() + 10;
		}
	}


 /****************************************************************************/
 /********************** VALIDATING JAVA VERSION *****************************/
 /****************************************************************************/

	/**
	 * Returns whether the Java version is passed the minimum Java version
	 * requirement or not. <p>
	 *
	 * Called this method before load the game to be sure the user machine has
	 * the minimum Java version to play the game.
	 *
	 * @see #MINIMUM_VERSION
	 */
	protected boolean validJavaVersion() {
		VALID_JAVA_VERSION = isValidVersion();

		return VALID_JAVA_VERSION;
	}

	private boolean isValidVersion() {
	try {
		StringTokenizer versionToken = new StringTokenizer(JAVA_VERSION, "."),
						minimumToken = new StringTokenizer(MINIMUM_VERSION, ".");
		int total = (versionToken.countTokens() > minimumToken.countTokens()) ?
					versionToken.countTokens() : minimumToken.countTokens();
		String version = "", minimum = "";

		String parsed = "";
		for (int i=0;i < total;i++) {
			char inc;
			try { inc = versionToken.nextToken().charAt(0);
			} catch (Exception e) { inc = '0'; }
			version += inc;

			try {
				inc = minimumToken.nextToken().charAt(0);
				if (parsed.length() > 0) parsed += ".";
				parsed += inc;
			} catch (Exception e) { inc = '0'; }
			minimum += inc;
		}

		int ver = Integer.parseInt(version),
			min = Integer.parseInt(minimum);
		MINIMUM_VERSION = parsed;

		return (ver >= min);

	} catch (Exception e) {
		System.err.println(
			"WARNING: MINIMUM_VERSION [" + MINIMUM_VERSION + "] and/or "+
			"JAVA_VERSION [" + JAVA_VERSION + "] value is not valid!"
		);

		return true;
	} }


	public String getGraphicsDescription() {
	try {
		return (gfx != null) ? gfx.getGraphicsDescription() :
							   super.getGraphicsDescription();

	} catch (Exception e) {
		return (gfx != null) ?
			gfx.getClass().toString() :
			this.getClass().toString();
	} }


 /****************************************************************************/
 /*************************** WINDOW LISTENER ********************************/
 /****************************************************************************/

    /**
     * If the user pressing frame close button while playing in
     * <code>WindowedMode</code> or <code>FullScreenMode</code>,
     * this method will receive the closing event. <p>
     *
     * In this implementation, pressing frame close button will instantly close
     * any playing game by calling the game
     * {@linkplain Game#finish() finish()} method. <p>
     *
     * To avoid the game closed when frame close button is pressed,
     * simply override this method or remove this listener
     * from the graphics engine :
     * <pre>
	 *    GameLoader game = new GameLoader();
	 *    game.setup(...);
	 *
	 *    BaseGraphics gfx = game.getGame().bsGraphics;
	 *
	 *    // only fullscreen and window mode the game is using frame
	 *    if (gfx instanceof FullScreenMode) {
	 *       // remove this listener
	 *       ((FullScreenMode) gfx).getFrame().removeWindowListener(game);
	 *    }
	 *
	 *    if (gfx instanceof WindowedMode) {
	 *       // remove this listener
	 *       ((WindowedMode) gfx).getFrame().removeWindowListener(game);
	 *    }
	 *
	 *    game.start();
     * </pre>
     */
	public void windowClosing(WindowEvent e) {
		if (game != null) {
			game.finish();
		}
	}

    /** Do nothing. */
	public void windowOpened(WindowEvent e) { }
	/** Do nothing. */
    public void windowClosed(WindowEvent e) { }
    /** Do nothing. */
    public void windowIconified(WindowEvent e) { }
    /** Do nothing. */
    public void windowDeiconified(WindowEvent e) { }
    /** Do nothing. */
    public void windowActivated(WindowEvent e) { }
    /** Do nothing. */
    public void windowDeactivated(WindowEvent e) { }

}